#!/usr/bin/python
# -*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__date__ = u"9 févr. 2010"
__author__ = u"INRIA pyLot"

__all__ = [u'CommandGroup',
           u'command_group_sample',
           u'merge_command_groups',
           u'split_command_group'
           ]

import sys
import time

from _pylot.minimal.path import Path
from _pylot.minimal.utils import (check_var_naming_convention, check_type,
                                check_iterable, arrange_alias, error, uprint,
                              check_implements)
from _pylot.minimal.commands import *

from _pylot.minimal.interfaces.i_streamwriter import IStreamWriter
from _pylot.minimal.default.impl_commonrun import CommonRun

class CommandGroup (CommonRun):
  u"""
  The CommandGroup class is used to define an ordered suite of simple commands.
  Commands can be shell commands, simple python commands or special instructions. 
  A command group can be run by a command group handler (also
  called process handler).
  The CommandGroup class has meta-informations like group identifier or
  alias.

  In a run, commands are launched in order and only if previous command has
  finished successfully.
  The CommandGroup class is independent of process execution implementation.

  CommandGroup allows to define dependencies between groups. For example, group2
  require group1 successful finish to start.

  Functions and methods exist to merge, split or edit command groups.

  The easiest way to create a CommandGroup is :
  
  >>> cg = CommandGroup(u'listing', alias=u'List files')
  >>> cg.addShellCmd(u'ls')
  
  To change directory in a command group, you must use the working_directory
  property. Here is an example :
  
  >>> cg.working_directory = u'/home'
  >>> cg.addShellCmd(u'ls')
  
  Now, shell command lists /home instead of current working directory.
  
  You can display text to user. How the text will be displayed depends on 
  command group handler used.
  
  >>> cg.addText(u'Hello')

  :var working_directory: (str or :class:`~pylot.core.Path`) change current working directory for next commands
  :var group_id: (str) group identifier
  :var alias: (str) group alias ("description")
  :var category: (str) Additional information. Ex: CMake and Make can share "Build" category.
  :var cmds: list of cmds in command group

  """

  def __init__ (self, group_id, alias=None, category=None):
    u"""
    :param group_id: group identifier
    :type group_id: str
    :param alias: group alias (or desc). If None, set value to group_id
    :type alias: str
    :param category: Additionnal information. Ex: CMake CommandGroup and Make CommandGroup can share "Build" category.
    :type category: None
    """
    CommonRun.__init__(self, None)

    check_type(u'group_id', group_id, unicode)
    check_var_naming_convention(group_id)
    check_type(u'alias', alias, unicode, none=True)
    check_type(u'category', category, unicode, none=True)

    self.group_id = group_id
    self.alias = arrange_alias(group_id, alias)
    self.category = category

    # if None, process_handler value is used
    self._stdout_mode = None
    self._stderr_mode = None

    #
    self._nice = 0

    self.cmds = []
    self.shellcmds = []
    self.cmds.append(SpecialCommand.StartGroup())
    self.cmds.append(SpecialCommand.EndGroup())
    self._wait_list = []

    self.thread = None
    self._progress = [0, 2] # cmd done, total

    self._stdout_writers = []
    self._stderr_writers = []
    self._textout_writers = []

  def _setstdout_writers (self, writer_list):
    for writer in writer_list :
      check_implements(u'writer', writer, IStreamWriter)
    self._stdout_writers = writer_list

  def _setstderr_writers (self, writer_list):
    for writer in writer_list :
      check_implements(u'writer', writer, IStreamWriter)
    self._stderr_writers = writer_list

  def _settextout_writers (self, writer_list):
    for writer in writer_list :
      check_implements(u'writer', writer, IStreamWriter)
    self._textout_writers = writer_list

  stdout_writers = property(
    fget=lambda self: self._stdout_writers,
    fset=_setstdout_writers
    )

  stderr_writers = property(
    fget=lambda self: self._stderr_writers,
    fset=_setstderr_writers
    )

  textout_writers = property(
    fget=lambda self: self._textout_writers,
    fset=_settextout_writers
    )

  def _checkOutputKeys (self, key_list):
    check_type(u'key_list', key_list, list)
    allowed_keys = (u'write', u'save')
    for key in key_list :
      if key not in allowed_keys or key is None :
        raise ValueError, error(tr_(u'Key %s not allowed. Available keys %s') % (
          key, u', '.join(allowed_keys)))

  def _setstdout_mode (self, key_list):
    if key_list is None :
      self._stdout_mode = None
    else :
      self._checkOutputKeys(key_list)
      self._stdout_mode = key_list

  def _setstderr_mode (self, key_list):
    if key_list is None :
      self._stderr_mode = None
    else :
      self._checkOutputKeys(key_list)
      self._stderr_mode = key_list

  stdout_mode = property(
    fget=lambda self: self._stdout_mode,
    fset=_setstdout_mode
    )

  stderr_mode = property(
    fget=lambda self: self._stderr_mode,
    fset=_setstderr_mode
    )

  def _setnice (self, nice):
    check_type(u'nice', nice, int)
    self._nice = nice

  nice = property(
    fget=lambda self: self._nice,
    fset=_setnice
    )

  def __len__ (self):
    return self.progress[1]

  def prettyPrint (self, level=1):
    u"""
    Returns a pretty string representing the command group

    >>> def f (a) : print a

    >>> cg2 = CommandGroup(u'subgroup')
    >>> cg2.addPythonCmd(f, [u'b'])

    >>> cg = CommandGroup(u'example', u'Pretty print example') 
    >>> cg.addPythonCmd(f, [u'a'])
    >>> cg.addCommandGroup(cg2)
    >>> cg.addShellCmd(u'ls', [u'-R', u'/'])
    
    >>> print cg.prettyPrint()
    <BLANKLINE>
    # Command Group : example (Pretty print example)
      (*) f (a)
      (*) f (b)
      (*) ls -R /
    <BLANKLINE>
    """
    check_type(u'level', level, int)

    tab = 0
    text = u'\n# Command Group : %s (%s)\n' % (self.group_id, self.alias)
    for cmd in self.cmds :
      if type(cmd) is SpecialCommand :
        if cmd.action == SpecialCommand.START_GROUP :
          tab += 1
        elif cmd.action == SpecialCommand.END_GROUP :
          tab -= 1
        elif cmd.action == SpecialCommand.SEP_GROUP :
          text += u'%s%s\n' % (u'  ' * tab, u'-' * 30)
      else :
        if tab < level :
          text += u'%s %s\n' % (u'  ' * tab, cmd)
        elif tab == level :
          text += u'%s(*) %s\n' % (u'  ' * tab, cmd)

    check_type(u'text', text, unicode)
    return text

  def resetCmds (self):
    u"""
    Resets command group cmds
    """
    for i in range (len(self.cmds)) :
      devnull = self.cmds.pop()
    self.cmds.append(SpecialCommand.StartGroup())
    self.cmds.append(SpecialCommand.EndGroup())
    for i in range (len(self.cmds)) :
      devnull = self.shellcmds.pop()
    self._progress[0] = 0
    self._progress[1] = 2
    return True

  def _addCmd (self, command):
    u"""
    .. warning :: 
      keep command working directory
    """
    check_type(u'command', command, CommonCommand)

    if isinstance(command, ShellCommand) :
      self.shellcmds.append(command)
    self.cmds.append(self.cmds[-1]) # move END_GROUP command
    self.cmds[-2] = command
    self._progress[1] += 1

  def findCommands (self, cmd_id):
    u"""
    returns a list of tuple (cmd index, cmd)
    cmd is a cmd with same cmd_id
    
    :param cmd_id: command identifier you want to search
    :type cmd_id: str
    """
    result = []
    for i, cmd in enumerate(self.cmds) :
      if cmd.cmd_id == cmd_id :
        result.append((i, cmd))
    return result

  def addCmd (self, command):
    u"""
    Adds a command object. Command object must be (or based on) TextCommand, 
    ManualCommand, SpecialCommand, ShellCommand or PythonCommand.
    
    You can use convenience methods addShellCmd, addPythonCmd, addText to do
    it more easily.

    Command.working_directory is set to current CommandGroup working_directory

    :param command: command
    :type command: Command object
    """
    check_type(u'command', command, CommonCommand)
    if type(command) not in (ShellCommand, PythonCommand,
                             TextCommand, ManualCommand,
                             SpecialCommand) :
      uprint(tr_(u'command: %s is not yet officially supported') % type(command),
           file=sys.stderr)


    command.working_directory = self.working_directory
    self._addCmd(command)

  def addShellCmd (self, cmd, args=[], cmd_id=None, alias=None):
    u"""
    Adds SHELL command to command group.

    This is only a convenience method that creates a ShellCommand object and
    calls addCmd methods.

    :param cmd: the shell command (ex: echo, sleep, mv, ls, ...)
    :type cmd: str
    :param args: list of shell arguments
    :type args: str list
    :param cmd_id: command identifier, must be short and complies with python variable syntax.
    :type cmd_id: str
    :param alias: command alias, used for pretty printing. if False, cmd_id is used
    :type alias: str
    """
    check_type(u'cmd', cmd, unicode)
    check_type(u'args', args, list)
    check_type(u'cmd_id', cmd_id, unicode, none=True)
    check_type(u'alias', alias, unicode, none=True)

    command = ShellCommand(cmd, args, cmd_id, alias)
    self.addCmd(command)

  def addPythonCmd (self, python_method, args=[], kargs={}, cmd_id=None,
                    alias=None):
    u"""
    Adds PYTHON command to Group

    This is only a convenience method that creates a PythonCommand object and
    call addCmd.
    
    .. warning ::

      ONLY add very simple python functions : 
        - one or two instructions
        - pure python
        - no C++ bindings
        - no threaded instructions
  
      These restrictions are due to potential thread issues in implementations.
      For example, QThread cannot handle complex python instructions.

    :param python_method: python method (ex math.sqrt)
    :type python_method: python object
    :param args: list of numberd arguments
    :type args: list
    :param kargs: dictionary of named arguments
    :type kargs: dict
    :param cmd_id: command identifier, must be short and complies with python variable syntax
    :type cmd_id: str
    :param alias: command alias, used for pretty printing. if False, cmd_id is used
    :type alias: str
    """
    check_type(u'args', args, list)
    check_type(u'kargs', kargs, dict)
    check_type(u'cmd_id', cmd_id, unicode, none=True)
    check_type(u'alias', alias, unicode, none=True)

    if cmd_id is None :
      cmd_id = unicode(python_method.__name__)
    command = PythonCommand(python_method, args, kargs, cmd_id, alias)
    self.addCmd(command)

  def addText (self, text, style=u'std'):
    u"""
    Adds a display text instruction

    :param text: display text
    :type text: str
    :param style: 'info', 'std', 'warning', 'alias'
    :type style: str
    """
    check_type(u'text', text, unicode)
    check_type(u'style', style, unicode)

    command = TextCommand(text, style)
    self.addCmd(command)

  def setWorkingDirectory (self, working_directory):
    u"""
    Changes working directory. 
    
    This is only a convenience method.
    Equivalent to CommandGroup.working_directory = working_directory
    
    @warning: all commands may not handle working_directory correctly.
    Please read ProcessHandler implementation documentation carefully.
    """
    check_type(u'working_directory', working_directory, [unicode, Path])
    self.working_directory = working_directory

  def addCommandGroup(self, command_group):
    u"""
    Inserts a command group in current CommandGroup
    
    .. warning ::
      commands of added command_group use their own working_directory and not
      current CommandGroup working_directory !
    """
    check_type(u'command_group', command_group, CommandGroup)

    for command in command_group.cmds[1:-1] : # ignore start/stop commands
      self._addCmd(command)

  def requireGroups (self, *args):
    u"""
    Groups passed in argument must-be finished before this group really start
    """
    for i, command_group in enumerate(args) :
      check_type(u'command_group #%d' % i, command_group, CommandGroup)
      if command_group not in self._wait_list :
        self._wait_list.append(command_group)

  def _getprogress (self):
    return self._progress

  def _getState (self):
    if self.cmds :
      if self.cmds[-1].state == self.Finished :
        return self.Finished
      elif self.cmds[0].state == self.NotRunning :
        return self.NotRunning
      else : # maybe a command fails
        for cmd in self.cmds :
          if cmd.state == self.Finished and cmd.return_code != 0 :
            # A command fail, group has finished
            self.cmds[-1].STATE = self.Finished
            return self.Finished
          elif cmd.state == self.Finished :
            continue
          else :
            return self.Running
        return self.Finished
    else :
      return self.NotRunning

  def _getReturnCode (self):
    if self.cmds :
      for cmd in self.cmds :
        if cmd.return_code not in (None, 0) :
          return cmd.return_code
        else :
          continue
      return cmd.return_code
    else :
      return None

  def _setWorkingDirectory (self, working_directory):
    self._working_directory = Path(working_directory).abspath()
    self.cmds[-1].working_directory = self.working_directory

  progress = property (fget=_getprogress)

def _test_print_args (seconds, num, verbose):
  if verbose :
    uprint(tr_(u'py%d: sleep %d seconds') % (num, seconds))
  time.sleep(seconds)
  uprint(tr_(u'py%d: wake up') % num)

def command_group_sample (pysleep=0, shellsleep=0, num=1,
                          pyfail=False, shellfail=False, fail=0,
                          verbose=False):
  u"""
  A CommandGroup generator.

  :param pysleep: python command sleep 'pysleep' seconds
  :type pysleep: int
  :param shellsleep: shell command sleep 'shellsleep' seconds
  :type shellsleep: int
  :param pyfail: python command fail with return code 1
  :param shellfail: shell command fail with return code 2
  :param fail: manual command fail with return code 'fail'
  :param verbose: display a text using TextCommand
  :type verbose: bool
  :param num: group identifier
  :type num: int
  :return: command group
  :rtype: CommandGroup 
  """
  cg = CommandGroup(u'cg_%d' % num, alias=u'Command Group %d' % num)

  # Text
  # ======
  if verbose :
    cg.addText(u'--- Starts "command_group_sample %d" ---' % num, u'std')

  # Python
  # ======
  if pyfail:
    cg.addPythonCmd(int, [u'a'], cmd_id=u'pythonfail')

  cg.addPythonCmd(_test_print_args, [pysleep, num, verbose],
                  cmd_id=u'print_args', alias=u'print1')

  # Failure
  # =======
  cg.addCmd(ManualCommand().failed(fail))

  # Shell
  # =====
  if verbose :
    cg.addShellCmd(u'echo', [u'sh%d: sleep %d seconds' % (num, shellsleep)],
                   u'echo1', u'echo1')

  if shellfail :
    cg.addShellCmd(u'ls', [u'/dossierquinexistepas'], u'shellfail')

  cg.addShellCmd(u'sleep', [shellsleep], u'sleep', u'sleep %d s' % shellsleep)
  cg.addShellCmd(u'echo', [u'sh%d: wake up' % num], u'echo2', u'echo2')

  return cg

def merge_command_groups(command_groups, group_id=None, alias=None, category=None):
  u"""
  :param command_groups: list of CommandGroup you want to merge. 
  :type command_groups: list
  """
  check_iterable(u'command_groups', command_groups)
  for i, command_group in enumerate(command_groups) :
    check_type(u'command_group #%d' % i, command_group, CommandGroup)

  check_type(u'group_id', group_id, unicode, none=True)
  check_type(u'alias', alias, unicode, none=True)
  check_type(u'category', category, unicode, none=True)

  if group_id is None :
    group_id = command_groups[0].group_id
  else :
    check_var_naming_convention(group_id)

  if alias is None :
    alias = command_groups[0].alias
  if category is None :
    category = command_groups[0].category

  merged_cg = CommandGroup(group_id, alias, category)
  group_nb = len(command_groups)
  for j, command_group in enumerate(command_groups) :
    merged_cg.working_directory = command_group.working_directory
    for i in range(1, len(command_group) - 1) :
      merged_cg._addCmd(command_group.cmds[i])
    if 0 <= j < group_nb - 1 :
      merged_cg._addCmd(SpecialCommand.SeparateGroup())

  return merged_cg

def split_command_group (command_group):
  u"""
  Tries to split a command group. If command_group cannot be split, return
  [command_group]

  :return: list of command group
  :rtype: list 
  """
  check_type(u'command_group', command_group, CommandGroup)
  raise NotImplementedError

if __name__ == '__main__':
  import doctest
  doctest.testmod()
